package com.jerome.fs.server.aggregation;

import com.jerome.fs.config.ConfigUtil;
import com.jerome.fs.entities.WeatherVo;
import com.jerome.fs.json.JsonParseException;
import com.jerome.fs.lamport.LamportClock;
import com.jerome.fs.log.Logger;
import com.jerome.fs.mq.MessageQueue;
import com.jerome.fs.server.aggregation.config.AggregationConfig;
import com.jerome.fs.server.aggregation.handler.AggregationLoggerHandler;
import com.jerome.fs.storage.DBF;
import com.jerome.fs.storage.ExpiringMap;
import com.jerome.fs.storage.FileUtil;
import com.jerome.fs.storage.RingBuffer;
import com.jerome.fs.util.JsonUtil;
import com.jerome.fs.util.LockUtil;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * Aggregation Server context util
 * Manage Aggregation Server global configuration, cache, and other information
 *
 * @author Jerome
 * @date 2023/8/8
 */
public class ContextUtil {

    /**
     * Local Lamport Clock
     */
    private volatile static LamportClock lamportClock = new LamportClock(0);
    /**
     * Used to store data uploaded by GET clients
     * WeatherVo map
     * Key: Weather id
     * Value: WeatherVo
     */
    private volatile static ExpiringMap<String, WeatherVo> expiringMap;
    /**
     * Message Queue
     */
    private volatile static MessageQueue<ServerMqEvent> mq;
    /**
     * Lock Util
     */
    private static LockUtil lockUtil;
    /**
     * Logger
     */
    private static Logger logger;
    /**
     * Configuration Information
     */
    private static AggregationConfig aggregationConfig = new AggregationConfig();
    /**
     * Incremental Synchronization RingBuffer
     */
    private static RingBuffer ringBuffer;
    /**
     * Slave Node Information
     */
    private volatile static ExpiringMap<String, String> nodeMap;

    /**
     * Initialize configuration information, this method must be executed first
     *
     * @param configPath
     */
    public synchronized static void init(String configPath) throws IOException, IllegalAccessException, InstantiationException {
        if (configPath != null && !"".equals(configPath)) {
            Properties properties = new Properties();
            try (InputStream input = new FileInputStream(configPath)) {
                properties.load(input);
            }
            aggregationConfig = ConfigUtil.build(properties, AggregationConfig.class);
        }
        File file = new File(aggregationConfig.getLogPath());
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    public synchronized static Logger getLogger() throws IOException {
        if (logger == null) {
            logger = new Logger(aggregationConfig.getLogPath(), aggregationConfig.getLogName(), 10 * 1024 * 1024L);
            // Add a logger handler
            logger.setLoggerHandler(new AggregationLoggerHandler());
        }
        return logger;
    }

    /**
     * Get message queue
     *
     * @return
     */
    public synchronized static MessageQueue<ServerMqEvent> getMq() {
        if (mq == null) {
            mq = new MessageQueue<>(10000);
        }
        return mq;
    }

    /**
     * Get expiring map
     *
     * @return
     */
    public synchronized static ExpiringMap<String, WeatherVo> getExpiringMap() {
        if (expiringMap == null) {
            expiringMap = new ExpiringMap<>(aggregationConfig.getExpiringTime());
        }
        return expiringMap;
    }

    public static String getLogfileName() {
        return aggregationConfig.getLogPath() + aggregationConfig.getLogName();
    }

    public static String getDBFFileName() {
        return aggregationConfig.getLogPath() + aggregationConfig.getDBFName();
    }

    public static LamportClock getLamportClock() {
        return lamportClock;
    }

    public static AggregationConfig getAggregationConfig() {
        return aggregationConfig;
    }

    public static RingBuffer getRingBuffer() {
        if (ringBuffer == null) {
            ringBuffer = new RingBuffer(aggregationConfig.getRingBufferSize());
        }
        return ringBuffer;
    }

    public synchronized static ExpiringMap<String, String> getNodeMap() {
        if (nodeMap == null) {
            nodeMap = new ExpiringMap<>(aggregationConfig.getHeartbeatExpiringTime());
        }
        return nodeMap;
    }

    /**
     * Serialize cache
     *
     * @return
     */
    public static synchronized void serializeDBF() throws IOException {
        DBF dbf = new DBF();
        dbf.setLatestTime(lamportClock.getLatestTime());
        getExpiringMap().refresh();
        dbf.setExpiringMap(getExpiringMap());
        FileUtil.serialize(dbf, ContextUtil.getDBFFileName());
    }

    /**
     * Restore data
     *
     * @throws ClassNotFoundException
     * @throws IOException
     * @throws JsonParseException
     * @throws InstantiationException
     */
    public static synchronized void restoreDBF() throws ClassNotFoundException, IOException, IllegalAccessException, JsonParseException, InstantiationException {
        File file = new File(ContextUtil.getDBFFileName());
        // Check if the file exists
        if (file.exists() && file.isFile()) {
            DBF o = (DBF) FileUtil.deserialize(ContextUtil.getDBFFileName());
            ContextUtil.updateExpiringMap(o.getExpiringMap());
            // Restore data from the log
            file = new File(ContextUtil.getLogfileName());
            if (file.exists() && file.isFile()) {
                try (BufferedReader in = new BufferedReader(new FileReader(file))) {
                    String str;
                    // Check if it's after the offset; data after the offset needs to be compared and refreshed
                    boolean offsetFlag = false;
                    while ((str = in.readLine()) != null) {
                        // Restore data in the format: lamport_clock:id:data_json
                        String[] split = str.split("\t");
                        if (split.length == 3) {
                            if (!offsetFlag) {
                                int lastTime = Integer.parseInt(split[0]);
                                // Compare the time for data updates
                                if (lastTime > o.getLatestTime()) {
                                    offsetFlag = true;
                                }
                            } else {
                                // Perform comparison and update
                                WeatherVo weatherNew = JsonUtil.readValue(split[2], WeatherVo.class);
                                WeatherVo weatherOld = getExpiringMap().get(weatherNew.getId());
                                if (weatherOld == null || weatherNew.getLatestTime() > weatherOld.getLatestTime()) {
                                    getExpiringMap().put(weatherNew.getId(), weatherNew);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Update expiring map
     *
     * @param expiringMap
     */
    public static void updateExpiringMap(ExpiringMap<String, WeatherVo> expiringMap) {
        int lastTime = 0;
        List<String> set = new ArrayList<>(expiringMap.keySet());
        for (String key : set) {
            WeatherVo weatherVo1 = expiringMap.get(key);
            if (weatherVo1 == null) {
                continue;
            }
            WeatherVo weatherVo2 = getExpiringMap().get(key);
            if (weatherVo2 == null || weatherVo1.getLatestTime() > weatherVo2.getLatestTime()) {
                getExpiringMap().put(key, weatherVo1);
                if (weatherVo1.getLatestTime() > lastTime) {
                    lastTime = weatherVo1.getLatestTime();
                }
            }
        }
        getLamportClock().tick(lastTime);
    }

    /**
     * Update expiring map
     *
     * @param weatherVos
     */
    public static void updateExpiringMap(List<WeatherVo> weatherVos) {
        for (WeatherVo weatherVo1 : weatherVos) {
            WeatherVo weatherVo2 = getExpiringMap().get(weatherVo1.getId());
            if (weatherVo2 == null || weatherVo1.getLatestTime() > weatherVo2.getLatestTime()) {
                getExpiringMap().put(weatherVo1.getId(), weatherVo1);
            }
        }
    }

    /**
     * Update nodeMap
     *
     * @param nodeMap
     */
    public static void updateNodeMap(ExpiringMap<String, String> nodeMap) {
        List<String> set = new ArrayList<>(nodeMap.keySet());
        for (String key : set) {
            String value = nodeMap.get(key);
            if (value == null) {
                continue;
            }
            getNodeMap().put(key, value);
        }
    }

    public synchronized static LockUtil getLockUtil() {
        if (lockUtil == null) {
            lockUtil = new LockUtil(getAggregationConfig().getExpiringTime());
        }
        return lockUtil;
    }
}
